package org.summerframework.component.httpclient;


import org.summerframework.core.util.config.ConfigUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.cookie.CookieSpecProvider;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.DefaultCookieSpecProvider;
import org.apache.http.impl.cookie.RFC6265CookieSpecProvider;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * httpclick工具类
 * <p/>
 * <p/>
 * 默认设置
 * <p/>
 * <p/>
 * http_client.connect_time_out=60000
 * http_client.socket_time_out=30000
 * http_client.max_total=20
 * http_client.max_per_route=100
 * <p/>
 * <p/>
 *
 * @author shichao
 */
@Slf4j
public class HttpClientUtil {

    private final static CloseableHttpClient HTTP_CLIENT;
    private static final Map<HttpClient, HttpClientContext> CLIENT_CONTEXT_MAP = new HashMap<>();
    private static RequestConfig config = RequestConfig.custom()
            .setConnectTimeout(Integer.parseInt(ConfigUtils.getProperty("http_client.connect_time_out", "600000")))
            .setSocketTimeout(Integer.parseInt(ConfigUtils.getProperty("http_client.socket_time_out", "600000"))).build();

    static {
        HTTP_CLIENT = createdHttpClient();
    }

    /**
     * get请求,需要登录信息
     *
     * @param url 请求地址
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:44:59
     */
    public static HttpResult getRequest(String url) {
        return getRequest(url, null, null);
    }

    /**
     * post请求,需要登录信息
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:44:59
     */
    public static HttpResult getRequest(String url, Map<String, Object> params) {
        return getRequest(url, params, null);
    }

    /**
     * post请求,需要登录信息
     *
     * @param url         请求地址
     * @param cookieStore 获取到登录信息后的登录cookie参数
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:44:59
     */
    public static HttpResult getRequest(String url, Map<String, Object> params, CookieStore cookieStore) {
        HttpParam httpParam = new HttpParam();

        httpParam.setUrl(url);
        httpParam.setMethod(RequestMethod.GET);
        httpParam.setCookieStore(cookieStore);
        httpParam.setParams(params);
        return request(httpParam);
    }

    /**
     * post请求,需要登录信息
     *
     * @param url 请求地址
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:44:59
     */
    public static HttpResult postRequest(String url) {
        return postRequest(url, null, null);
    }

    /**
     * post请求,需要登录信息
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:44:59
     */
    public static HttpResult postRequest(String url, Map<String, Object> params) {
        return postRequest(url, params, null);
    }

    /**
     * post请求,需要登录信息
     *
     * @param url         请求地址
     * @param params      请求参数
     * @param cookieStore 获取到登录信息后的登录cookie参数
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:44:59
     */
    public static HttpResult postRequest(String url, Map<String, Object> params, CookieStore cookieStore) {
        HttpParam httpParam = new HttpParam();

        httpParam.setUrl(url);
        httpParam.setMethod(RequestMethod.POST);
        httpParam.setCookieStore(cookieStore);
        httpParam.setParams(params);
        return request(httpParam);
    }

    /**
     * 统一请求方式, 默认以get方式请求 暂时只实现get与post方式
     *
     * @param httpParam 请求参数
     * @return 返回值
     * @author 石超
     * @date 2015-4-14 下午3:53:02
     */
    public static HttpResult request(final HttpParam httpParam) {
        HttpResult httpResult = null;
        HttpRequestBase request = null;

        if (StringUtils.isBlank(httpParam.getUri())) {
            return new HttpResult("url is null");
        }

        if (httpParam.getCustomHttpClient() == null) {
            httpParam.setCustomHttpClient(HTTP_CLIENT);
        }

        long start = System.currentTimeMillis();

        try {
            URL url = new URL(httpParam.getUri());
            CloseableHttpClient httpClient = httpParam.getCustomHttpClient();
            final HttpClientContext context = getContext(httpClient, httpParam.getCookieStore());
            request = initParam(httpParam);

            httpResult = httpClient.execute(request, response -> fillResponse(context, httpParam), context);
        } catch (IOException e) {
            log.error("时间:" + (System.currentTimeMillis() - start) + httpParam.getUri(), e);

            if (request != null && !request.isAborted()) {
                request.abort();
            }

            return new HttpResult("请求失败");
        } finally {
            if (request != null) {
                request.releaseConnection();
            }
        }

        return httpResult;
    }

    /**
     * 初始化请求参数与请求方式
     *
     * @param httpParam 请求参数
     *                  {@link HttpParam}
     * @throws IOException
     */
    private static HttpRequestBase initParam(HttpParam httpParam) throws IOException {
        HttpRequestBase httpRequest = null;

        switch (httpParam.getMethod()) {
            case GET:
                httpRequest = new HttpGet(httpParam.getUrl());
                break;
            case POST:
                httpRequest = new HttpPost(httpParam.getUrl());
                break;
            case HEAD:
                httpRequest = new HttpHead(httpParam.getUrl());
                break;
            case PUT:
                httpRequest = new HttpPut(httpParam.getUrl());
                break;
            case PATCH:
                httpRequest = new HttpPatch(httpParam.getUrl());
                break;
            case DELETE:
                httpRequest = new HttpDelete(httpParam.getUrl());
                break;
            case OPTIONS:
                httpRequest = new HttpOptions(httpParam.getUrl());
                break;
            case TRACE:
                httpRequest = new HttpTrace(httpParam.getUrl());
                break;
            default:
                throw new IllegalArgumentException("Invalid HTTP method: " + httpParam.getMethod());
        }

        if (httpParam.getHeader() != null) {
            for (Entry<String, String> entry : httpParam.getHeader().entrySet()) {
                httpRequest.addHeader(entry.getKey(), entry.getValue());
            }
        }

        if (httpRequest instanceof HttpEntityEnclosingRequestBase) {
            HttpEntityEnclosingRequestBase httpEntityEnclosingRequestBase = (HttpEntityEnclosingRequestBase) httpRequest;
            httpEntityEnclosingRequestBase.setURI(URI.create(httpParam.getUrl()));

            // 设置contentBody 为了防止设置了Entity导致url缺失
            if (httpParam.getEntity() != null) {
                httpEntityEnclosingRequestBase.setURI(URI.create(httpParam.getUri()));
                httpEntityEnclosingRequestBase.setEntity(httpParam.getEntity());
            } else if (httpParam.hasFile()) {
                // 请求是否有参数
                if (!MapUtils.isEmpty(httpParam.getParams()) || !MapUtils.isEmpty(httpParam.getContentBodies())) {
                    // 参数是否含有文件
                    // 对请求的表单域进行填充
                    MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
                    entityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);

                    for (Entry<String, List<String>> param : httpParam.getParams().entrySet()) {
                        for (String str : param.getValue()) {
                            entityBuilder.addTextBody(param.getKey(), str);
                        }
                    }

                    for (String key : httpParam.getContentBodies().keySet()) {
                        int count = 0;
                        List<ContentBody> list = httpParam.getContentBodies().get(key);

                        if (CollectionUtils.isEmpty(list)) {
                            continue;
                        }

                        for (ContentBody contentBody : list) {
                            if (list.size() > 1) {
                                entityBuilder.addPart(key + "[" + (count++) + "]", contentBody);
                            } else {
                                entityBuilder.addPart(key, contentBody);
                            }
                        }
                    }

                    entityBuilder.setLaxMode();
                    httpEntityEnclosingRequestBase.setEntity(entityBuilder.build());
                }
            } else {
                List<NameValuePair> nvps = new ArrayList<NameValuePair>();

                for (Entry<String, List<String>> param : httpParam.getParams().entrySet()) {
                    for (String str : param.getValue()) {
                        nvps.add(new BasicNameValuePair(param.getKey(), str));
                    }
                }

                UrlEncodedFormEntity uefEntity = new UrlEncodedFormEntity(nvps, httpParam.getCharset());
                httpEntityEnclosingRequestBase.setEntity(uefEntity);
            }
        } else {
            httpRequest.setURI(URI.create(httpParam.getUri()));
        }

        httpRequest.setConfig(config);

        return httpRequest;
    }

    /**
     * 完成响应
     *
     * @param context   上下文
     * @param httpParam 请求参数
     * @return 请求结果 {@link HttpResult}
     * @throws ParseException
     * @throws IOException
     * @author 石超
     * @date 2015-4-14 下午2:23:51
     */

    private static HttpResult fillResponse(HttpClientContext context, HttpParam httpParam) throws ParseException, IOException {
        HttpResponse response = context.getResponse();
        HttpResult httpResult = null;

        httpResult = fillResult(response, context);

        // 日志信息
        log.debug("-------------------------------------------------------");
        log.debug("request line: " + context.getRequest().getRequestLine());
        // 获取响应消息实体
        HttpEntity entity = response.getEntity();
        // 响应状态
        log.debug("status: " + response.getStatusLine());
        log.debug("url: " + httpParam.getUri());
        log.debug("headers:\n");
        httpParam.getHeader().forEach((key, value) -> {
            log.debug("{} :{}", "\t" + key, value);
        });
        log.debug("\n");
        log.debug("headers:\n");
        HeaderIterator iterator = response.headerIterator();

        while (iterator.hasNext()) {
            log.debug("\t" + iterator.next());
        }

        // 判断响应实体是否为空
        if (entity != null) {
            String responseString = httpResult.getContext();
            log.debug("response length:" + responseString.length());
            log.debug("response content:\n" + responseString.replace("\r\n", ""));
        }

        log.debug("-------------------------------------------------------");

        boolean isMoved = (context.getResponse().getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_PERMANENTLY || response.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) && httpParam.isFlag();

        if (isMoved) {
            Header locationHeader = response.getFirstHeader("location");

            String location = null;

            if (locationHeader != null) {
                location = locationHeader.getValue();
                if (httpParam.getStopUrl() != null && location.contains(httpParam.getStopUrl())) {
                    return null;
                }

                httpParam.resetUrl(location);

                httpParam.setMethod(RequestMethod.GET);
                // 用跳转后的页面重新请求。
                httpResult = request(httpParam);
            }
        }

        return httpResult;
    }

    /**
     * 设置HttpClientContext
     *
     * @param cookieStore
     * @return
     * @author 石超
     * @date 2015-4-14 下午1:26:17
     */
    public static HttpClientContext getContext(HttpClient httpClient, CookieStore cookieStore) {
        HttpClientContext httpClientContext = CLIENT_CONTEXT_MAP.compute(httpClient, (k, oldValue) -> {
            HttpClientContext context = HttpClientContext.create();
            Registry<CookieSpecProvider> registry = RegistryBuilder
                    .<CookieSpecProvider>create()
                    .register(CookieSpecs.DEFAULT, new DefaultCookieSpecProvider())
                    .register(CookieSpecs.STANDARD, new RFC6265CookieSpecProvider()).build();
            context.setCookieSpecRegistry(registry);

            return context;
        });

        if (cookieStore != null) {
            httpClientContext.setCookieStore(cookieStore);
        }

        return httpClientContext;
    }

    /**
     * 创建HttpClient对象
     *
     * @return
     * @author 石超
     * @date 2015-4-14 下午1:26:31
     */
    public static CloseableHttpClient createdHttpClient() {
        return createdHttpClient(HttpClientContext.create());
    }

    /**
     * 创建HttpClient对象 根据HttpClientContext存储上下文
     *
     * @param context
     * @return
     * @author 石超
     * @date 2015-4-14 下午1:26:46
     */
    public static CloseableHttpClient createdHttpClient(HttpClientContext context) {

        SSLConnectionSocketFactory sslsf = null;

        try {
            //信任所有
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true).build();

            sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            sslsf = SSLConnectionSocketFactory.getSocketFactory();
        }

        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslsf)
                .build());

        //连接池最大并发连接数
        cm.setMaxTotal(Integer.parseInt(ConfigUtils.getProperty("http_client.max_total", "2000")));
        //单路由最大并发数
        cm.setDefaultMaxPerRoute(Integer.parseInt(ConfigUtils.getProperty("http_client.max_per_route", "100")));

        return HttpClients.custom()
                .setDefaultCookieStore(context.getCookieStore())
                .setDefaultRequestConfig(config)
                .setConnectionManager(cm)
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
                .build();
    }

    /**
     * 封装返回集合
     *
     * @param response 请求后响应对象
     * @param context  HttpClientContext
     * @return
     * @throws ParseException
     * @throws IOException
     * @author 石超
     * @date 2015-4-14 下午1:27:25
     */
    private static HttpResult fillResult(HttpResponse response, HttpClientContext context) throws ParseException, IOException {
        HttpEntity entity = response.getEntity();
        HttpResult result = new HttpResult(EntityUtils.toByteArray(entity), ContentType.getOrDefault(entity));

        Header[] headers = response.getAllHeaders();

        for (Header header : headers) {
            result.addHeader(header.getName(), header.getValue());
        }

        result.setCookieStore(context.getCookieStore());
        result.setStatusLine(response.getStatusLine());
        return result;
    }
}
